Chapter 13 · Backend Operations

Configuration
Management

The DNA of your application — how the same code behaves differently across environments without ever touching the codebase.


01 · DEFINITION

What is Configuration Management?

Config management is the systematic approach to organize, store, access and maintain all the settings of our backend app.

If we go by the definition, configuration management (or just "config management") is the systematic approach to organize, store, access, or basically maintaining all the settings of our backend app. The key word there is settings.

You can think of configuration as the DNA of your application. It decides how your code runs in different environments. The exact same application code can behave in completely different ways depending purely on its configuration — and that's by design.

The Common Misconception

When you say "configuration management" to most people, the first impression — the first idea that hits the mind — is storing things like:

Those are the things that come to mind whenever we talk about config management. But thinking like that misses a lot of the scope.

The Car Analogy

Thinking config management is just secrets and DB URLs is like saying "a car is just about the engine." The engine is definitely the most important part of a car — but you're still missing out on 90% of the other features of the car. Similarly, configuration management has a lot of other dimensions.

The Full Scope of Config Management

Configuration management actually covers a wide range of application behaviors:

There is a lot of scope for config management. It's not just limited to your database URL or secret keys — it affects a lot of important behaviors of your backend application. That's exactly why it's such an important topic.


02 · WORKED EXAMPLE

The E-Commerce Platform Example

Let's say you are building an e-commerce platform, and you're working on the backend. Your configuration might include a whole range of things, each with very different characteristics. Let's walk through every category that such a backend would need.

E-commerce backend configuration categories E-Commerce Backend Config DB Connection host, port, user, pass, timeout Payment API Keys e.g. Stripe keys Feature Flags new vs old checkout Performance Tuning connection pool size Security Settings session timeouts Business Rules max order amount
Fig 1.1 — The six configuration categories of an e-commerce backend

Breaking down each category in detail:

Why These Configs Have Different Characteristics

Each of these configurations has fundamentally different characteristics — and recognising these differences is what drives every decision about how to store and protect them:

CharacteristicExamplesImplication
Sensitive / secretDB password, Stripe key, JWT secretMust never leak — can cause major loss/damage
Non-secret but behavior-controllingLog level, feature flags, pool sizeControls how the app behaves at runtime
Frequently changingFeature flags during a rolloutNeed a fast, dynamic update path
Rarely changingBusiness rules (monthly/quarterly)Can be stored more statically
Same across all environmentsCertain business rulesShared config
Different per environmentDB host, pool size, log levelEnvironment-specific config

Some are sensitive and supposed to be kept secret — leaking them might cause a lot of damage and loss. Some are not secretive but they control how your application behaves while it's running. Some change frequently; others change once a month or once a quarter. Some stay the same across development, staging and production; others differ depending on the environment. This diversity is the core reason config management needs a real strategy.


03 · THE CHALLENGE

The Distributed Systems Challenge

On top of all that diversity sits one big challenge: distributed systems. Most of our modern applications — all our modern backends — don't run in isolation anymore. Most of them are now part of a complex distributed system.

A distributed system consists of multiple moving parts:

All these different integration points — where you connect a separate service to your main backend — they all require configuration. Your backend obviously needs to know how to connect to these services. But on top of that, it also needs to know:

All of this depends on different environments, different settings, and different configs — which we can control from different sources. This is the reason that, because most modern backends run on distributed systems, configuration management becomes such an important part of your application.

Why Backend Config Stakes Are So High

The stakes are higher for backend engineers because they handle the core business logic and data — pretty much the most important part of any platform or enterprise.

A misconfigured frontend might show a wrong feature, a wrong dialogue it's not supposed to show, or redirect to a wrong route — annoying, but recoverable.

A misconfigured backend can expose customer data, process payments incorrectly, or bring down your entire platform if an important part is affected. The blast radius is enormous.

And backend systems run in diverse environments — different cloud providers, on-premises servers, containers, serverless functions, edge functions — and all of these have their own requirements for different configurations. There's no single uniform target, which makes a systematic approach essential.


04 · THE ANTI-PATTERN

Configuration Chaos

If you don't have a systematic approach — if you don't have a dedicated pipeline, a dedicated strategy to manage your config — you will end up with what we call configuration chaos.

Configuration chaos means that instead of controlling configs from a centralized point, you have a mess. Here's what it looks like:

Hard-coded scattered values

Configuration values are hard-coded and scattered throughout your codebase instead of living in one centralized place.

Inconsistent behavior

Your application behaves inconsistently across different environments because configs aren't managed uniformly.

Security vulnerabilities

Exposed secrets leak into your codebase, version control, and logs — creating real security holes.

Debugging nightmare

You can't reproduce issues because there's no centralized way of managing config. You don't know which config caused which break in production.

This last point is the killer: debugging your application becomes a nightmare because you cannot reproduce the issues. You don't really know what exactly happened, what particular config caused a particular issue, or what particular config caused a break in production. Centralized config management is what gives you that traceability.


05 · TYPES

Types of Configuration

Not all configurations are created equal. Understanding the different types of configuration data is very important for choosing the right storage mechanism, security measures, and access patterns — who can access what, and in which environments a particular config should be accessible. Let's go through each type.

5.1 — Application Settings

The most common kind of config you'll see in most backend apps. This includes:

Timeout Example — The AI Image Case

Say you set a server timeout of 60 seconds, and you initiate a request that generates an AI image (returned to the frontend in base64). If AI image generation takes ~80 seconds on average, but your server timeout is 60 seconds, your request gets dropped — you'll get a 504 Gateway Timeout status. The timeout config directly determines whether a legitimate slow request succeeds or fails.

5.2 — Database Configuration

Database config includes all the details your application needs to connect to your database:

All these parameters combined let you construct a connection URL, which your application uses at runtime to connect to the database. It might also have other parameters — like query timeouts (how long a database query should run before it times out).

5.3 — External Services

External services come in many types, and each has its own configuration needs — usually some kind of API key:

It doesn't matter which provider you use — you'll most probably have some kind of API key in your config that you need to cater to.

5.4 — Feature Flags

Feature flags exist so that you can dynamically enable or disable particular features of your application without changing code.

Feature flag A/B testing by region Feature Flag new_checkout_enabled US users → NEW checkout flag = true India users → OLD checkout flag = false conditional rollout by location
Fig 2.1 — Feature flag enabling new checkout for US users while India keeps the old flow (A/B testing)

The classic example: you've built a new checkout flow (using a different API) and you don't want to roll it out to all users at once. You want to do A/B testing — only enable the new feature for a particular segment of users depending on their location. For instance, enable the new checkout for users from the US, but keep the old flow for users from India. Feature flags make this dynamic enabling/disabling possible — and they also need config management.

5.5 — Other Config Types

Beyond the four main categories, there are several more:


06 · STORAGE

Sources of Config (Storage)

With all these diverse config needs, we also have to think about where to store the configs. Where you store your configurations depends on a lot of parameters — security, speed, and your environment. Let's look at the main storage mechanisms.

6.1 — Environment Variables

The most common kind of config storage that most people have seen. If you've worked with backends before, you've encountered .env files. These are most common among Node.js apps, but they're also supported in Python, Go, and any language you build backends in.

How it works locally

In local environments, you have a file called .env. A very famous library called dotenv takes all your environment variables from the .env file and loads them into your operating system's environment.

Important Distinction

Environment variables are a feature provided by your operating system — this is not something you build. Libraries like dotenv simply take values from files and load them into your environment automatically, instead of you doing it manually with export commands.

How it works in deployment

If you're not local — say you're deploying in a containerized environment using Kubernetes — those technologies also support loading environment variables while deploying your application. Here's the typical cloud deployment workflow:

Cloud deployment env var loading workflow Deployment triggers Fetch secrets from Vault / Param Store Azure / GCP Secret Mgr at a particular stage Load into env OS environment App starts reads env, runs Deployment → fetch from secret manager → load into environment → app boots
Fig 3.1 — Cloud deployment env var workflow: deploy triggers fetch from a secrets service, loads into the environment, app reads on startup

The deployment triggers; at a particular stage it fetches all your environment variables from a service — Vault (HashiCorp), Parameter Store, Azure, or Google Secret Manager (any cloud provider that offers secrets management) — and loads them into your environment. Then when your application starts, it reads those environment variables and functions accordingly. This is the most common storage mechanism for configurations.

6.2 — Config Files

The second kind of storage is files, which come in different formats:

FormatComments support?Notes
JSONNoMajor disadvantage — cannot add comments, so harder to share knowledge in a team
YAMLYesMore famous for configs these days; supports comments for team knowledge-sharing
TOMLYesA newer standard, also heavily used for managing configurations

One of the major disadvantages of JSON is that you cannot add comments — JSON has no comment support. That's why most people use YAML: you can add your configuration and add comments so it's easier for your team to share knowledge. TOML is a newer standard that's also popular.

Real-World Examples

Authelia (an authentication provider written in Go) uses a configuration.yaml file holding server settings, log level (debug), storage, notification details, identity details, regulations, session settings — all in a single YAML file.

Apache Answer (incubator, open source) also uses config.yaml in many places — app settings like which port the server runs on, databases (SQLite for local environments), and Swagger UI config. Storing configurations in YAML is very common across open-source repositories.

6.3 — Key-Value Stores & Cloud Providers

Key-Value Stores

Beyond simple env values, there are dedicated cloud-native tools like Consul and etcd. Depending on your use case you can use any of them. Key-value stores are pretty lightweight and simple to use compared to more complicated, hierarchical data structures for config management. They act mostly like your environment variables.

Dedicated Cloud Secret Managers

Here the major players are:

These are dedicated services for config and secrets management that most companies use, because deployments today are distributed — Kubernetes, autoscaling, sometimes multiple cloud providers at once for scaling needs. Having all your configurations centralized in a tool like HashiCorp Vault — which already supports all these different environments, with dedicated docs and integrations — makes the integration as easy as possible. Using an external provider makes a lot of sense if you're running heavy user traffic in production.

Hybrid Strategies

Most of the time, teams actually use hybrid strategies. You have a loading phase where you construct your runtime application settings by fetching configs from multiple places, each with a priority:

Hybrid config loading with priority 1. AWS Parameter Store highest priority 2. config.yaml file middle priority 3. Environment variables fallback / defaults Loading phase merge by priority Runtime Settings single config object
Fig 3.2 — Hybrid config loading: multiple sources merged by priority into one runtime settings object

For example, in the first priority you might fetch from AWS Parameter Store, then from a config.yaml, then from environment variables. You pre-decide which source has higher priority, and depending on the environment you load those configs conditionally. This gives you flexibility: secrets from a vault, structured settings from a file, and overrides from env vars — all merged into one runtime config.


07 · ENVIRONMENTS

Why Config Differs Per Environment

One natural question: why do we have different configs depending on the environment we're running in? The answer is simple — each environment has a set of priorities that the other environments don't need or shouldn't have.

Four environments and their priorities Development developer productivity + debugging Test automated validation + quality assurance Staging mirror production catch issues early Production reliability, security + performance
Fig 4.1 — The four environments and their distinct priorities

Walking through each environment's priority:

The Whole Point — Code Stays the Same

We create configs keeping these priorities in mind. The application code remains the same across all environments — but depending on the config, the behavior changes. That's exactly what we want. Without changing the code (which we'd have to do if we were hard-coding things), centralized config management means we never have to touch our application code. We just change the config, and that reflects in the behavior.

Worked Example — Database Pool Size Across Environments

Take the database connection pool size as a concrete example of how the same setting differs per environment:

EnvironmentMax Pool SizeReasoning
Development10Works fine locally; modern high-end dev machines have plenty of capacity for testing/development
Staging2Deliberately minimal to save cloud costs — we accept some delay since it's used by devs/testers
Production50Must handle a large user base and traffic spikes, so a higher pool size is needed

In production you set the pool size to 50 because you have to cater to a large user base and prepare for traffic spikes. In development, 10 works fine. The interesting one is staging: even though staging's main priority is to mirror production as much as possible to catch issues early, running a system that functions exactly like production would cost the same in cloud bills — and that's not something most teams want.

The Cost Tradeoff in Staging

Staging has a secondary priority: minimizing cloud costs. Since staging is usually used only by developers and testers, you can make decisions like keeping the database pool size to a minimum (e.g. 2). You accommodate some delay there, but you save a lot of money on cloud costs. This is a deliberate, config-driven tradeoff — and it's possible precisely because the pool size lives in config, not code.


08 · SECURITY

Configuration Security

These are fairly obvious points, but for the sake of covering the full scope, they're worth stating explicitly. There are five key security practices for config management.

8.1 — Never Hardcode Secrets

If you have secrets like your production database URL, the API key of your Clerk service, your email delivery service, or your payment processing service — you should never hardcode those secrets in your codebase. This is so obvious it barely needs explaining: hardcoded secrets end up in version control, get exposed in logs, and become permanent liabilities.

8.2 — Use a Cloud Secrets Manager

If possible, always go for a cloud secrets management service — HashiCorp Vault, AWS Parameter Store, Azure Key Vault, or Google Secret Manager. It's always a good idea to "over-engineer" when it comes to the security of your backend application.

These services already handle critical security features automatically:

Encryption at rest and in transit Secret Manager encrypted at rest 🔒 encrypted in transit (API call) Your Infra decrypt w/ private key App uses config decrypted, in memory Encrypted both at rest (storage) and in transit (transfer)
Fig 5.1 — Secret managers encrypt configs at rest AND in transit; decryption happens with your private key in your infra

These are essential security features, and when you use a cloud provider like Vault or Parameter Store, all of this is already taken care of — you don't have to think about it.

8.3 — Access Control (Least Privilege)

If you have a fairly large developer team, take time to strategize who should have access to which configs, following the principle of least privilege:

Following the least privilege principle, you strategize your access control so that each person only has access to what they actually need.

8.4 — Rotation

You should periodically rotate all your configs — API key secrets, JWT secrets, and similar sensitive values — so that they're not very prone to leakage. Regular rotation limits the damage window if a secret is ever compromised.

8.5 — Validation (The Most Important Point)

Most of the time, teams don't validate their environment variables or whatever source their configs are coming from. With dotenv, for example, you load all the variables into your environment and just access them via process.env.WHATEVER — with no checks.

The One Thing to Take Away From This Whole Topic

If you take just one thing away: always validate your configs, no matter where they come from. Validate at startup — before your application starts, right after it's deployed. Use a proper validation library:

TypeScript backend → use Zod
Go backend → use go-playground/validator

Use whatever validation library your language/framework offers, and properly validate every variable — which ones are mandatory, which are optional, which have application-code defaults.

Why does this matter so much? It's learned the hard way. If you have a mandatory environment variable requirement but fail to provide it, your production system either breaks or behaves in a strange way because it doesn't have access to a particular environment variable — and it's not easy to spot. Validating at startup turns a silent, mysterious production failure into a loud, immediate, obvious error message that tells you exactly which config is missing.


09 · CODE

Code Examples

Below are practical implementations of config loading and validation in Go and Python — covering env vars, per-environment loading, hybrid sources, and startup validation.

9.1 — Config Loading & Validation in Go

config.go · Go + go-playground/validator + godotenv
package config

import (
    "fmt"
    "log"
    "os"
    "strconv"

    "github.com/go-playground/validator/v10"
    "github.com/joho/godotenv"
)

// Config holds all runtime application settings.
// The `validate` tags enforce rules at startup — this is the
// single most important safeguard for config management.
type Config struct {
    // Application settings
    Port     int    `validate:"required,min=1,max=65535"`
    LogLevel string `validate:"required,oneof=debug info warn error"`
    Env      string `validate:"required,oneof=development staging production"`

    // Database config (sensitive)
    DBHost     string `validate:"required"`
    DBPort     int    `validate:"required"`
    DBUser     string `validate:"required"`
    DBPassword string `validate:"required"`
    DBName     string `validate:"required"`
    DBPoolSize int    `validate:"required,min=1"`

    // External services (sensitive)
    StripeAPIKey string `validate:"required"`

    // Feature flags (optional, default false)
    NewCheckoutEnabled bool
}

// DatabaseURL constructs the connection URL from the parts.
func (c *Config) DatabaseURL() string {
    return fmt.Sprintf(
        "postgres://%s:%s@%s:%d/%s",
        c.DBUser, c.DBPassword, c.DBHost, c.DBPort, c.DBName,
    )
}

// Load reads env vars, applies defaults, then VALIDATES before returning.
// Called once at startup — fail loudly here, never silently in production.
func Load() (*Config, error) {
    // In local dev, load .env into the OS environment.
    // In production this is a no-op (vars already injected by the platform).
    _ = godotenv.Load()

    cfg := &Config{
        Port:               getEnvInt("PORT", 8080),        // default 8080
        LogLevel:           getEnv("LOG_LEVEL", "info"),    // default info
        Env:                getEnv("APP_ENV", "development"),
        DBHost:             os.Getenv("DB_HOST"),
        DBPort:             getEnvInt("DB_PORT", 5432),
        DBUser:             os.Getenv("DB_USER"),
        DBPassword:         os.Getenv("DB_PASSWORD"),
        DBName:             os.Getenv("DB_NAME"),
        DBPoolSize:         getEnvInt("DB_POOL_SIZE", 10),  // dev=10, prod=50
        StripeAPIKey:       os.Getenv("STRIPE_API_KEY"),
        NewCheckoutEnabled: getEnv("NEW_CHECKOUT", "false") == "true",
    }

    // THE critical step — validate everything before the app boots
    if err := validator.New().Struct(cfg); err != nil {
        return nil, fmt.Errorf("config validation failed: %w", err)
    }

    return cfg, nil
}

func getEnv(key, fallback string) string {
    if v := os.Getenv(key); v != "" {
        return v
    }
    return fallback
}

func getEnvInt(key string, fallback int) int {
    if v := os.Getenv(key); v != "" {
        if n, err := strconv.Atoi(v); err == nil {
            return n
        }
    }
    return fallback
}

// Usage in main.go:
//   cfg, err := config.Load()
//   if err != nil { log.Fatal(err) }  // crash early, crash loud

9.2 — Config Loading & Validation in Python

config.py · Python + pydantic-settings (the Python equivalent of Zod)
from enum import Enum
from functools import lru_cache

from pydantic import Field, computed_field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Environment(str, Enum):
    DEVELOPMENT = "development"
    STAGING = "staging"
    PRODUCTION = "production"


class Settings(BaseSettings):
    """
    All runtime config. pydantic VALIDATES every field automatically
    when the object is constructed — mandatory fields without a default
    raise an error immediately at startup, not silently in production.
    """
    model_config = SettingsConfigDict(
        env_file=".env",        # load .env in local dev
        env_file_encoding="utf-8",
        case_sensitive=False,
    )

    # Application settings (with sensible defaults)
    port: int = Field(default=8080, ge=1, le=65535)
    log_level: str = Field(default="info", pattern="^(debug|info|warn|error)$")
    app_env: Environment = Environment.DEVELOPMENT

    # Database config — mandatory, no defaults (will fail if missing)
    db_host: str
    db_port: int = 5432
    db_user: str
    db_password: str          # sensitive — never hardcoded
    db_name: str
    db_pool_size: int = Field(default=10, ge=1)  # dev=10, prod=50, staging=2

    # External services — mandatory secret
    stripe_api_key: str

    # Feature flag — optional, defaults off
    new_checkout_enabled: bool = False

    @computed_field
    @property
    def database_url(self) -> str:
        return (
            f"postgresql://{self.db_user}:{self.db_password}"
            f"@{self.db_host}:{self.db_port}/{self.db_name}"
        )


@lru_cache  # load & validate once, reuse everywhere (singleton)
def get_settings() -> Settings:
    # Construction triggers validation. If a mandatory var is
    # missing, pydantic raises here — at startup, loudly.
    return Settings()


# Usage:
#   settings = get_settings()   # crashes early if config invalid
#   print(settings.database_url)
#   if settings.new_checkout_enabled: ...

9.3 — Example YAML Config File

For non-secret, structured settings, a YAML file (with comments, unlike JSON) is a common choice — as seen in real codebases like Authelia and Apache Answer:

config.yaml · structured app settings (non-secret)
# Server settings — note: YAML supports comments, JSON does not
server:
  port: 8080
  timeout_seconds: 60      # raise this for slow ops like AI image gen

log:
  level: debug             # debug in dev, info in production

database:
  host: localhost
  port: 5432
  name: ecommerce
  pool_size: 10            # dev=10, staging=2, prod=50
  query_timeout_seconds: 30

# Feature flags — dynamically enable/disable features
features:
  new_checkout_enabled: false
  rollout_regions: ["US"]   # A/B test: US gets new checkout

# Business rules — centralized, not hardcoded
business:
  max_order_amount: 500000

# NOTE: secrets (db password, Stripe key, JWT secret) do NOT
# belong here — those come from a secrets manager / env vars.

REFERENCES

Further Reading & Documentation

Secrets Management Tools

Key-Value / Config Stores

Validation Libraries

Config File Formats & Libraries

MDN Web Docs

BACKEND ENGINEERING FIELD MANUAL · V2 · CHAPTER 13 · CONFIGURATION MANAGEMENT
Notes compiled from lecture transcript · Go + Python examples · Vault/MDN references inline